<?php
/** OLD NOTE
 *   Router:
      - identify the language of request
      - identify the module (including finding the default module) which will handle request
      - identify operation name (using Nodes or directly from path)
      - initialize controller by module and operation name
*     The extensions may: identify areas, rewrite module names, redirect depending on hostname/GeoIP/etc
*
*   All exceptions thrown by router must lead to a 404 page or when required, redirect to login
 *
 */

class Yz_Router_Default
    extends Yz_Class
{

    const DEFAULT_MODULE = 'index';
    const ERROR_MODULE = 'error';

    /** not to repeat in sublcasses: one instance of Router allowed per-application
     *  @var self
     */
    protected static $instance;
    /**
     *  @var Yz_Array
     */
    protected  $arguments;
    /**
     *  @var Yz_Exception
     */
    protected  $exception;
    /**
     *  @var Yz_HTTP_Request_Interface
     */
    protected  $request;
    /**
     *  @var Yz_HTTP_Response_Interface
     */
    protected  $response;
    /**
     *  @var bool
     */
    protected  $isPOST;

    /**
     *  @var string
     */
    protected  $path;

    /** The reference to current module
     *  @var Module_Interface
     */
    protected $module;
    /** The reference to current controller
     *  @var Controller_Interface
     */
    protected $controller;
    /** The selector of current module
     *  @var string
     */
    protected $moduleSelector;
    /** The selector of current language
     *  @var string
     */
    protected $languageSelector;
    /** The selector of current controller
     *  @var string
     */
    protected $controllerSelector;



    /********************************
     *      Public methods          *
     *******************************/

    public static function getInstance()
    {
        debug();

        // NOTE using self, to ensure only one type of session per application
        $inst =& self::$instance;
        if ( null === $inst ) {
            $inst = new static;
        }
        return $inst;
    }

    /** test if the route method is POST
     *  @return bool
     */
    public function isPOST()
    {
        $p =& $this->isPOST;
        if (null === $p) {
            $p = $this->getRequest()->isPOST();
        }
        return $p;
    }

    /**
     *  @param Yz_HTTP_Request_Interface
     *  @param Yz_HTTP_Response_Interface
     *  @return Yz_HTTP_Response_Interface
     */
    public function routeRequest(
        Yz_HTTP_Request_Interface $rq,
        Yz_HTTP_Response_Interface $rs
    )
    {
        debug();

        // {{ prevent subsequent calls
        static $routed = false;
        if ($routed) {
            throw Yz_Router_Exception::_new();
        }
        $routed = true;
        // }}

        $this->request = $rq;

        $ctlr = $this->doRoute($rq->getPath());

        return $rs->setView($ctlr->getView());
    }


    /**
     *  @param string $path null means use the path from HTTP Request
     *  @return Yz_Controller_Interface
     */
    public function route( $path, Yz_Array $args=null )
    {
        debug();
        $path = Yz_String::test($path);
        return $this->doRoute($path, $args);
    }


    /***************************
     *     Protected methods   *
     ***************************/

    /** forbid direct instantiation
     *  ...
     */
    protected function __construct()
    {
        debug();
    }


    /** do the real routing job
     *  @param string $path
     *  @return Yz_HTTP_Response_Interface
     */
    protected function doRoute( $path, $args=null )
    {
        debug("Route path: '{$path}'");
        $this->path = $path;
        $this->arguments = $this->pathToArray($path);
        if ($args) {
            $this->arguments->append($args);
        }
        try {
            return $this
                ->identifyLanguage()
                ->identifyModule()
                ->identifyController()
                ->executeController()
            ;
        } catch (Yz_Exception $ex) {
            return $this->_errorHandler($ex);
        }
    }


    /**
     *  working on Arguments array
     *
     */
    protected function identifyLanguage()
    {
        debug();
        $this->languageSelector = "";
        $args = $this->arguments;
        if (! $args->isEmpty()) {
            $selector = $args[0];
            debug("Testing for language '$selector'");
            if ( I18N::isInstalledLanguage($selector) ) {
                I18N::setCurrentLanguage($selector);
                $this->languageSelector = $selector;

                $args->shift();
            }
        }
        return $this;
    }

    /**
     *  working on Arguments array
     *  @return this
     */
    protected function identifyModule()
    {
        debug();
        $args = $this->arguments;

        // thoroughly identify module
        if ($args->isEmpty()) {
            $module = $this->selectDefaultModule();
        } else {
            $module = $this->selectModule($args[0]);
            $this->moduleSelector = $args->shift();
        }
        $this->module = $module;
        return $this;
    }

    /**
     *  working on Arguments array
     *  @return this
     */
    protected function identifyController()
    {
        debug();
        $args = $this->arguments;
        $module = $this->module;
        $controller = null;
        if (! $args->isEmpty()) {
            $ctrlName = Yz_String::toCamelCase($args[0]);
            if (Yz_String::isValidIdentifier($ctrlName)
                    && $module->hasController($ctrlName)) {
                //$sel = $args->shift();
                $controller = $module->selectController($ctrlName);

                // real selector
                $this->controllerSelector = $args->shift();
            }
        }
        if (null === $controller) {
            $controller = $module->selectDefaultController();
        }
        $this->controller = $controller;
        return $this;
    }

    /**
     *  @return Yz_Controller_Interface
     */
    protected function executeController()
    {
        $this->testControllerNotRepeated($this->controller);

        if ($this->request->isPOST()) {
            $r = $this->controller->doPost($this->arguments, new Yz_Hash($_POST));
        } else {
            $r = $this->controller->doGet($this->arguments);
        }
        switch (true) {
            case false === $r:
                // restart routing process
                return $this->doRoute('', $this->arguments);
            case is_string($r):
                // select controller by the name $r
                $this->controller = $this->module->selectController($r);
                return $this
                    ->executeController()
                ;
            case $r instanceof Yz_Controller_Interface:
                return $r;
            default:
                throw new Yz_Router_Exception("Unrecognized return value from controller {$this->controller}");
        }

        // never gets here
        return /*Yz_Controller_Interface*/;
    }

    /** split path string into non-empty components (arguments) by '/'
     *  @param string $path
     *  @return Yz_Array
     */
    protected function pathToArray($path)
    {
        debug();

        // ### is filtering faster than preg_split ?
        return new Yz_Array( preg_split("~/+~", $path, null, PREG_SPLIT_NO_EMPTY) );
    }


    /** handle exceptions
     *  @return
     */
    protected function _errorHandler($ex)
    {
        debug($ex->toString(true));
        $this->exception = $ex;
        $handled = false;
        if ($this->controller) {
            $handled = $this->controller->doError($ex);
        }
        if (false === $handled && $this->module) {
            $handled = $this->module->selectErrorController();
            if (false !== $handled) {
                $handled = $handled->doError($ex);
            }
        }
        if (false === $handled) {
            $handled = Yz::selectModule(static::ERROR_MODULE)
                ->selectDefaultController()
                ->doError($ex)
            ;
        }
        return $handled;
    }


    /**
     *  @return Yz_Module_WebModule_Interface
     *  @throw Yz_Router_Exception_ResourceNotFound
     */
    protected function selectModule($moduleName)
    {
        $moduleName = Yz_String::toCamelCase($moduleName);
        $module = false;
        if (Yz_String::isValidIdentifier($moduleName)) {
            try {
                $module = Yz::selectModule($moduleName);
            } catch (Yz_Exception_ModuleNotFound $ex) {
                // $module remains false
            }
        }
        // TODO possibly load some module for the case of `bad module names`
        // NOTE: can this be done in error handler ???
        if (! ($module instanceof Yz_Module_WebModule_Interface)) {
            throw Yz_Router_Exception_ResourceNotFound::_new(array(
                'resource' => $this->path,
            ));
        }
        return $module;
    }

    /**
     *  @return Yz_Module_WebModule_Interface
     */
    protected function selectDefaultModule()
    {
        $module = Yz::selectModule( static::DEFAULT_MODULE );
        assert('$module instanceof Yz_Module_WebModule_Interface');
        return $module;
    }


    /** test whether a controller has not been executed yet
     *  @param Yz_Controller_Interface
     *  @return this
     */
    protected function testControllerNotRepeated(Yz_Controller_Interface $ctlr)
    {
        static $called = array();
        $myclass = $ctlr->className();
        if (isset($called[$myclass])) {
            throw new Yz_Router_Exception_RepeatedControllerExecution();
        }
        $called[$myclass] = 1;
        return $this;
    }

}
